using System;
using System.Collections.Generic;
using System.Linq;
using TestCases.Assertions;
using TestCases.CompareAssert.Core;
using TestCases.CompareAssert.Core.Helpers;
using TestCases.CompareAssert.Core.Interfaces;
using TestCases.CompareAssert.Core.Builders;
using TestCases.Utils;

namespace TestCases.Tests.Helpers
{
    public class TestResultBuilder: IResultBuilder, IObjectExpression
    {
        private readonly CompareResult _result;
        private readonly IAssertion _assertion;
        
        private ObjectSide _expressionSide = ObjectSide.None;
        private string _message;
        private object _args;
        
        
        public string ExpectedTemplate { get; set; }
        public object ExpectedArgs { get; set; }
        
        private string SideString
        {
            get
            {
                switch (_expressionSide)
                {
                    case ObjectSide.Left:
                        return "<{LPath}>";
                        
                    case ObjectSide.Right:
                        return "<{RPath}>";
                        
                    case ObjectSide.Both:
                        return "<{LPath}> and <{RPath}>";
                }
                _assertion.Fail("Invalid expression side");
                throw new ApplicationException("Should never reach here");
            }
        }
        

        public TestResultBuilder(CompareResult result)
        {
            _result = result;
            _assertion = AssertionUtils.Instance;
            _message = ResultBuilder.GetDefaultTemplate(_result.Status);
        }

        public IObjectExpression Left
        {
            get
            {
                _expressionSide = ObjectSide.Left;
                return this;
            }
        }

        public IObjectExpression Right
        {
            get
            {
                _expressionSide = ObjectSide.Right;
                return this;
            }
        }

        public IObjectExpression Both
        {
            get
            {
                _expressionSide = ObjectSide.Both;
                return this;
            }
        }
        
        public IResultBuilder Message(string message)
        {
            _message = message;
            return this;
        }

        public IResultBuilder With(object args)
        {
            _args = args;            
            return this;
        }

        public IResultBuilder Status(CompareStatus status)
        {
            _result.Status = status;
            return this;
        }

        public CompareStatus GetStatus()
        {
            return _result.Status;
        }

        public string GetMessage()
        {
            return _message;
        }

        public object GetArgs()
        {
            return _args;
        }

        private void AssertArgument(IDictionary<string, object> args, string name, object expected)
        {
            object output;
            if (args.TryGetValue(name, out output))
            {
                Assert(Equals(output, expected),
                    "\n\nArguments <{Name}> are different. \n\tActual:<{Actual}>. \n\tExpected:<{Expected}>. ".
                        FormatWith(new {Name = name, Expected = output.ToValueString(), Actual = expected.ToValueString()}));
                args.Remove(name);
            }            
        }
                
        private void Assert(bool expression, string message)
        {
            if (!expression)
            {
                Fail(message);
            }
        }
        
        private void Fail(string message)
        {
            _assertion.Fail(message + "\n\nResult trace: \n\t" + _result.FormatResult() + "\n");
        }

        public ICompareResult GetResult()
        {
            _result.Message = ResultBuilder.FormatMessage(_result, _message, _args);
            if (ExpectedTemplate != null)            
            {
                
                Assert(Equals(_message, ExpectedTemplate),
                   ("\n\nMessage templates are different. \n\t"
                  + "Actual:<{ActualTemplate}>.\n\t"
                  + "Expected:<{ExpectedTemplate}>.").
                  FormatWith(new
                  {
                      ActualTemplate = _message,
                      ActualArgs = _args,
                      ExpectedTemplate,
                      ExpectedArgs
                  }));
            }
            
            if (ExpectedArgs != null)
            {

                var expectedArgs = new Dictionary<string, object>();
                ExpectedArgs.ToDictionary(expectedArgs);
                
                AssertArgument(expectedArgs, "LPath", _result.Left.Path);
                AssertArgument(expectedArgs, "RPath", _result.Right.Path);
                AssertArgument(expectedArgs, "LValue", _result.Left.Object);
                AssertArgument(expectedArgs, "RValue", _result.Right.Object);
                AssertArgument(expectedArgs, "LType", _result.Left.Object.GetSafeType());
                AssertArgument(expectedArgs, "RType", _result.Right.Object.GetSafeType());
                AssertArgument(expectedArgs, "Status", _result.Status);

                var actualArgs = new Dictionary<string, object>();
                _args.ToDictionary(actualArgs);

                var arguments = expectedArgs.Keys.Union(actualArgs.Keys);
                foreach (var argument in arguments)
                {
                    if (!expectedArgs.ContainsKey(argument))
                    {
                        Fail("\n\nActual arguments have extra argument <{Name}>. ".FormatWith(new {Name = argument}));
                    }
                    
                    if (!actualArgs.ContainsKey(argument))
                    {
                        Fail("\n\nActual arguments doesn't have <{Name}> argument. ".FormatWith(new {Name = argument}));
                    }

                    object actualValue = actualArgs[argument], expectedValue = expectedArgs[argument];
                    Assert(Equals(actualValue, expectedValue), 
                        "\n\nArguments <{Name}> are different. \n\tActual:<{Actual}>. \n\tExpected:<{Expected}>. ".
                        FormatWith(new {Name = argument, Actual = actualValue, Expected = expectedValue}));
                                        
                }
                                
            } 
            return _result;
        }

        public IResultBuilder IsNot<T>()
        {
            return IsNot(typeof(T));
        }

        public IResultBuilder IsNot(Type type)
        {
            return IsNot(type.ToString());            
        }

        public IResultBuilder IsNot(string type)
        {
            return Message(SideString + " is not " + type + ".");
        }
    }
}